/*=============================================================================
    jibits/widget.js
    Copyright (C) 2008-2009, Donald W. Griffin
    All rights reserved.
    [MIT license :: see license.txt for details]
=============================================================================*/
$module("jibits.widget");

$requires("zjs.ext");

$requires("jibits.dom");

$namespace(jibits, {

Widget : $class(function ()
{
    function poll (widget)
    {
        widget.pollNow(); // preserve indirection to pollNow (allow override)
    }

    var body =
    {
        ctor : function (id)
        {
            $super(arguments, this);

            this.id = id;
            this.pos = jibits.Positioning.TL;
            this.poller = poll.head(this);

            this.connect();
        },

        connect : function ()
        {
            var el = this.elem();
            if (!el)
                return;
            jibits.addMeta(el, { widget : this });
            this.onConnect();
        },

        onConnect : function ()
        {
            $super(arguments).call(this);
        },

        destroy : function ()
        {
            var el = this.elem();
            if (el)
                delete jibits.getMeta(el)["widget"];

            this.poller.nevermind();
            if (this.monitors)
                this.monitors = {};
        },

        /**
        Returns the DOM element associated with this widget.
        @return The DOM element associated with this widget.
        */
        elem : function ()
        {
            return document.getElementById(this.id);
        },

        beginMod : function ()
        {
            if (this._mod)
                ++this._mod.count;
            else
                this._mod = { count: 1, changed: false };
            return this;
        },

        endMod : function (nochange)
        {
            this._mod.changed |= !nochange;
            if (--this._mod.count > 0)
                return this;

            // by leaving _mod and ++count we ensure that pollSoon cannot
            // kick off this process recursively...
            ++this._mod.count;
            if (this._mod.changed)
                this.pollSoon();
            delete this._mod; // all clear!
            return this;
        },

        pollLater : function (sec)
        {
            this.poller.later(sec);
        },

        pollNow : function ()
        {
            this.poller.nevermind();
            if (!this.elem())
            {
                this.pollLater(0.1);
                return;
            }

            this.sync();

            if (!this.monitors)
                return;

            var fn, monitors = this.monitors, ids = [];
            for (var n in this.monitors)
                ids.push(n);

            $foreach(ids, function (id)
            {
                if ((fn = monitors[id]))
                    fn();
            });
        },

        pollSoon : function ()
        {
            //this.pollerLater();
            this.pollNow();
        },

        monitor : function (id, fn)
        {
            if (!this.monitors)
                this.monitors = { };
            this.monitors[id] = fn;
        },

        unmonitor : function (id)
        {
            if (this.monitors)
                delete this.monitors[id];
        },

        sync : function ()
        {
            $super(arguments).call(this);
        },

        //-----------------------------
        // visibility - these all call setVisible to allow derived types to
        //      hook into this ever-important change.

        hide : function ()
        {
            return this.setVisible(false);
        },

        show : function ()
        {
            return this.setVisible(true);
        },

        toggleVisible : function ()
        {
            return this.setVisible(!this.isVisible());
        },

        //-----------------------------
        // size & position

        X : function () { return this.getTotalOffset().x; },
        Y : function () { return this.getTotalOffset().y; },
        W : function () { return this.getSize().w; },
        H : function () { return this.getSize().h; },

        placeAdjacent : function (elp, ref, refp)
        {
            jibits.placeAdjacent(this.elem(), elp, ref, refp, this.pos);
            return this;
        },

        placeCenter : function (ref, coords)
        {
            /*
function center(oNode){
   oNode.style.setExpression("left","getCenter(this)");
}
function getCenter(oNode){
   var oParent=oNode.parentElement;
   return (oParent.offsetWidth/2 - oNode.offsetWidth/2);
}
             */
            jibits.placeCenter(this.elem(), ref, coords, this.pos);
            return this;
        },

        moveTo : function (rect)
        {
            jibits.moveTo(this.elem(), rect, this.pos);
            return this;
        },

        remove : function ()
        {
            jibits.removeElem(this.id); // causes destroy method to be called
        },

        //-----------------------------
        // events

        /**
        Returns this widget's master Ticket. This Ticket is automatically
        destroyed when the element is removed from the DOM. Other destroyable
        objects or cleanup functions can be added to this ticket. The Ticket is
        created on the first call to this method.

        @return The master Ticket for this widget.
        */
        getTicket : function ()
        {
            if (!this.ticket)
            {
                this.ticket = jibits.eventMgr().newTicket();
                jibits.removeSubscribe(this.elem(),
                                this.ticket.destroy.bind(this.ticket));
            }

            return this.ticket;
        },

        /**
        Subscribes for one or more events on a given element. The element ID is
        required (not the element object). The hanlder method will be wrapped
        using jibits.listener, so see that method for details.

        The events can be either an array of event names or a single name. As in
        all cases, event names do not have the "on" prefix.

        This method returns a Ticket object that can be used to disconnect all
        the event handlers established by this call. To do so, call the destroy
        method of the returned Ticket. Otherwise, the Ticket will be destroyed
        automatically when this widget is destroyed or the element is removed
        from the DOM (which calls this widget's destroy method).

        @param id The ID of the element to which to subscribe.
        @param handler The event handler method.
        @param events The event name (string) or names (string[]).
        @return The Ticket managing the new event subscriptions.
        */
        listen : function (id, handler, events)
        {
            var ticket, eventMgr = jibits.eventMgr();

            handler = jibits.listener(handler, id);

            if (!(events instanceof Array))
                ticket = eventMgr.listen(id, events, handler);
            else
                $foreach(events, function (event)
                {
                    var t = eventMgr.listen(id, event, handler);
                    if (ticket)
                        ticket.add(t);
                    else
                        ticket = t;
                });

            this.getTicket().add(ticket);
            return ticket;
        },

        /**
        Subscribes for one or more events on this widget's element. The handler
        and events arguments are the same as with the listen method, as is the
        cleanup strategy.

        @param handler The event handler method.
        @param events The event name (string) or names (string[]).
        @return The Ticket managing the new event subscriptions.
        */
        listenSelf : function (handler, events)
        {
            return this.listen(this.id, handler, events);
        },

        //-----------------------------
        // z-order

        /**
        Moves this element above the reference element.

        @param ref The element to be above.
        @return This widget object.
        */
        moveAbove : function (ref)
        {
            jibits.moveAbove(this.elem(), $elem(ref));
            return this;
        },

        /**
        Moves this element below the reference element.

        @param ref The element to be below.
        @return This widget object.
        */
        moveBelow : function (ref)
        {
            jibits.moveBelow(this.elem(), $elem(ref));
            return this;
        },

        //-----------------------------
        // misc

        enable : function (b)
        {
            var el = this.elem();
            if (el)
                el.disabled = !b && (arguments.length != 0);
            return this;
        },

        /**
        Sets the focus to this widget's element.
        */
        focus : function ()
        {
            var el = this.elem();

            if (el && el.focus instanceof Function)
                try
                {
                    el.focus();
                }
                catch (e) { }
        },

        isChecked : function ()
        {
            var el = this.elem();
            return el && el.checked;
        },

        isEnabled : function ()
        {
            var el = this.elem();
            return el && !el.disabled;
        },

        setCheck : function (b)
        {
            var el = this.elem();
            if (el)
                el.checked = b || (arguments.length == 0);
            return this;
        },

        wrapDiv : function (wrap, attribs)
        {
            this.createDiv(attribs, wrap);
            return this;
        }
    };

    // Many methods simply delegate to DOM methods after first resolving this
    // Widget to an Element and then return whatever that DOM method returns.
    // We don't cache the result of "jibits[s]" because that would prevent any
    // replacement/override of the target method from having its proper effect.
    //
    $foreach(["getAttr", "hasAttr", "getSize", "getPosOffset", "getPosOffsetRect",
              "getScrollOffset", "getTotalOffset", "getTotalOffsetRect",
              "getViewportOffset", "getOpacity", "getStyle", "isVisible",
              "getClasses", "hasClass", "getTag", "isPos", "isPosAbs",
              "isPosFix", "isPosRel", "isPosStatic", "getOffsetParent"],
        function (s)
        {
            $assert(jibits[s], "s=", s);
            $assert(jibits[s].getArgumentNames().length <= 3);
            body[s] = function (a, b)
            {
                return jibits[s](this.elem(), a, b); // preserve indirection
            };
        })

    // Many methods also delegate to DOM methods after first resolving this
    // Widget to an Element and then return this Widget (whereas the DOM method
    // returns the Element).
    //
    $foreach(["setAttr", "setOpacity", "setStyle", "setVisible",
              "addClasses", "modifyClasses", "removeClasses",
              "moveToTop", "scrollTo", "cleanWhitespace"],
        function (s)
        {
            $assert(jibits[s], "s=", s);
            $assert(jibits[s].getArgumentNames().length <= 3);
            body[s] = function (a, b)
            {
                jibits[s](this.elem(), a, b); // preserve indirection
                return this;
            };
        })

    return body;
}()),

parseDocument : function (doc)
{
    doc = doc || document;
    jibits.parseWidgetSubTree(doc.body);
},

parseWidget : function ()
{
    var hoverRegex = /\bhoverhack=(?:1|true|on|yes)\b/i;

    return function (el, lazy)
    {
        el = $elem(el);
        var cn = el.className;
        if (cn.search(hoverRegex) >= 0)
        {
            jibits.hoverHack(el);
            el.className = cn = cn.replace(hoverRegex, "");
        }

        if (!el.id || zjs.isEmpty(cn)) // cannot be a widget w/o an ID
            return null;

        var meta = jibits.getMeta(el, lazy), ret = meta && meta.widget;

        if (!ret)
        {
            var decl = jibits.parseWidgetDecl(el.className);

            if (decl.type || !lazy)
            {
                if (decl.fixed)
                    el.className = decl.fixed;

                ret = new (decl.type || jibits.Widget)(el.id, decl.config);

                (meta || jibits.getMeta(el)).widget = ret;
            }
        }

        return ret;
    };
}(),

parseWidgetTree : function (el)
{
    if (!jibits.parseWidget(el, true))
        jibits.parseWidgetSubTree(el);
},

parseWidgetSubTree : function (el)
{
    el = $elem(el);

    for (var c = el.firstChild; c; c = c.nextSibling)
        if (c.nodeType == 1) // if (element)
            jibits.parseWidgetTree(c);
},

/**
Parses a space-separated string looking for widget declarations. The basic form
of these are:

        widget=foo.bar.MyWidget[@confg.obj] (token=value)*

The token is just an identifier. The value can be any of:

        'string' | true | false | integer | float | @variable

The returned object contains the following properties describing the declaration:

    {
        type : foo.bar.MyWidget,

        fixed : "", // the input string minus recognized elements

        config :  // has config.obj as prototype object
        {
            token : value
        }
    }

The "widget=" component is primary, regardless of its placement in the string. If
present, the value of the widget is assumed to be a class name and is looked up
from the global scope. That is, "foo" in the above example is assumed to be a
namespace in the global scope, with sub-namespace "bar" and class "MyWidget".

The optional "config.obj" component is used to indicate the base configuration
object for the widget. To reuse that object, a new object is chained with it via
its prototype chain. The text "config.obj" is resolved in the context of a given
context object (ctx). This defaults to the global scope.

All components of the string that appear to be an assignment are captured and
added to the config object. Any "@variable" values are resolved in the context of
the given context object (the global scope by default).

The above recognized components are removed from the string and the resulting
elements are joined with spaces and stored in the "fixed" property.
*/
parseWidgetDecl : function ()
{
    // value = 'string' | true | false | [+/-] ### | [+/-] ###.### | @code
    var _attrRegex =
        /^([^=]+)\=(?:(?:\'([^']*)\')|(true)|(false)|([+-]?\d+)|([+-]?\d+\.\d+)|(?:[@]([^@]+)))$/i;
    var _widgetRegex = /^widget\=([^@]+)(?:[@](.+))?$/i;

    return function (decl, ctx)
    {
        var ret = { config: {} }, props = decl.split(" "), m, v;
        ctx = ctx || $namespace.global;

        for (var i = props.length; i-- > 0; )
            if ((m = _widgetRegex.exec(props[i])))
            {
                ret.type = zjs.deref($namespace.global, m[1]);

                if (m[2] && (v = zjs.deref(ctx, m[2])))
                    ret.config = zjs.chain(v);

                props.splice(i, 1);
                break;
            }

        for (i = props.length; i-- > 0; )
            if ((m = _attrRegex.exec(props[i])))
            {
                if (m[7]) // if @
                    v = zjs.deref(ctx, m[7]);
                else if (m[6])
                    v = parseFloat(m[6]);
                else if (m[5])
                    v = parseInt(m[5]);
                else if (m[4])
                    v = false;
                else if (m[3])
                    v = true;
                else
                    v = m[2]; // 'foo'

                ret.config[m[1]] = v;
                props.splice(i, 1);
            }

        var s = props.join(" ");
        if (s != decl)
            ret.fixed = s;

        return ret;
    }
}(),

// modification notifiers

beginMod : $override(function (el)
{
    $super(arguments, this);
    var w = $widget(el, true);
    if (w)
        w.beginMod();
}),

endMod : $override(function (el, nochange)
{
    $super(arguments, this);
    var w = $widget(el, true);
    if (w)
        w.endMod(nochange);
}),

wasMod : $override(function (el)
{
    $super(arguments, this);
    var w = $widget(el, true);
    if (w)
        w.beginMod().endMod();
})

}); // namespace jibits

function $widget (id, nocreate)
{
    if (jibits.isWidget(id))
        return id;

    var el = $elem(id), meta = el ? jibits.getMeta(el, nocreate) : null;
    if (meta && meta.widget)
        return meta.widget;
    return nocreate ? null : jibits.parseWidget(el);
}
